package com.ssy.lingxi.contract.esign.api;

import cn.hutool.json.JSONUtil;
import com.ssy.lingxi.contract.esign.EsignProperties;
import com.ssy.lingxi.contract.esign.constant.EsignConstants;
import com.ssy.lingxi.contract.esign.constant.RequestMethod;
import com.ssy.lingxi.contract.esign.exception.EsignError;
import com.ssy.lingxi.contract.esign.exception.EsignException;
import com.ssy.lingxi.contract.esign.token.AccessToken;
import com.ssy.lingxi.contract.esign.token.EsignCache;
import com.ssy.lingxi.contract.esign.token.EsignLock;
import lombok.extern.slf4j.Slf4j;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 鉴权tokenAPI
 * @author yzc
 * @version 2.0.0
 * @date 2020/8/26
 */
@Slf4j
public class AuthToken {

    private final EsignLock esignLock;
    private final EsignCache esignCache;
    private final EsignProperties properties;

    public AuthToken(EsignLock esignLock, EsignCache esignCache, EsignProperties properties) {
        this.esignLock = esignLock;
        this.esignCache = esignCache;
        this.properties = properties;
    }

    // ======================================鉴权认证=====================================

    /**
     * 获取鉴权token
     * @return 认证token
     */
    public AccessToken getToken() throws EsignException {
        String resourcePath = "/v1/oauth2/access_token";
        RequestMethod method = RequestMethod.GET;

        // 请求
        Map<String, Object> param = new HashMap<>();
        param.put("appId", properties.getAppId());
        param.put("secret", properties.getAppSecret());
        param.put("grantType", "client_credentials");

        EsignRequest esignRequest = new EsignRequest(method, resourcePath, param, properties);
        String execute = EsignResource.execute(esignRequest);

        return JSONUtil.toBean(execute, AccessToken.class);
    }

    /**
     * 刷新鉴权Token
     * @return 认证token
     */
    public AccessToken refreshToken(String refreshToken) throws EsignException {
        String resourcePath = "/v1/oauth2/refresh_token";
        RequestMethod method = RequestMethod.GET;

        // 请求
        Map<String, Object> param = new HashMap<>();
        param.put("appId", properties.getAppId());
        param.put("refreshToken", refreshToken);
        param.put("grantType", "refresh_token");

        EsignRequest esignRequest = new EsignRequest(method, resourcePath, param, properties);
        String execute = EsignResource.execute(esignRequest);

        return JSONUtil.toBean(execute, AccessToken.class);
    }

    /**
     * 获取缓存token
     * @return accessToken
     */
    public String getCacheToken() throws EsignException {
        AccessToken cacheToken = (AccessToken) esignCache.get(EsignCache.TOKEN_KEY);
        if (Objects.nonNull(cacheToken)) {
            // 检查有效
            if (checkExpireTime(cacheToken)) {
                return cacheToken.getToken();
            }

            // 加锁, 防止并发请求
            Object lock = esignLock.lock(EsignLock.TOKEN_LOCK_KEY);
            if (Objects.isNull(lock)) {
                throw new EsignException(EsignError.builder().errMsg("业务繁忙, 请稍后再试").build());
            }
            try {
                // double check
                cacheToken = (AccessToken) esignCache.get(EsignCache.TOKEN_KEY);
                if (checkExpireTime(cacheToken)) {
                    return cacheToken.getToken();
                }

                // 刷新token
                AccessToken accessToken = null;
                try {
                    accessToken = refreshToken(cacheToken.getRefreshToken());
                } catch (EsignException e) {
                    if (EsignConstants.SUCCESS != e.getError().getErrCode()) {
                        // 重试请求token
                        accessToken = getAccessTokenAndCache();
                        return accessToken.getToken();
                    }

                    throw new EsignException(e.getError());
                }

                long millisecond = Long.parseLong(accessToken.getExpiresIn());
                esignCache.put(EsignCache.TOKEN_KEY, accessToken, millisecond);

                return accessToken.getToken();
            } finally {
                esignLock.releaseLock(lock);
            }
        }

        // 加锁, 防止并发请求
        Object lock = esignLock.lock(EsignLock.TOKEN_LOCK_KEY);
        if (Objects.isNull(lock)) {
            throw new EsignException(EsignError.builder().errMsg("业务繁忙, 请稍后再试").build());
        }
        try {
            // double check
            cacheToken = (AccessToken) esignCache.get(EsignCache.TOKEN_KEY);
            if (checkExpireTime(cacheToken)) {
                return cacheToken.getToken();
            }

            // 请求token
            AccessToken accessToken = getAccessTokenAndCache();

            return accessToken.getToken();
        } finally {
            esignLock.releaseLock(lock);
        }
    }

    /**
     * 重新获取accessToken,并缓存
     * @return accessToken
     */
    private AccessToken getAccessTokenAndCache() throws EsignException {
        AccessToken accessToken = null;
        try {
            accessToken = getToken();
        } catch (EsignException esignException) {
            throw new EsignException(esignException.getError());
        }

        long millisecond = Long.parseLong(accessToken.getExpiresIn());
        esignCache.put(EsignCache.TOKEN_KEY, accessToken, millisecond);

        return accessToken;
    }

    /**
     * 获取AccessToken,并缓存
     * @return token
     */
    public String getTokenAndCache() throws EsignException {
        // 加锁, 防止并发请求
        Object lock = esignLock.lock(EsignLock.TOKEN_LOCK_KEY);
        if (Objects.isNull(lock)) {
            throw new EsignException(EsignError.builder().errMsg("业务繁忙, 请稍后再试").build());
        }
        AccessToken accessToken = null;
        try {
            try {
                accessToken = getToken();
            } catch (EsignException esignException) {
                throw new EsignException(esignException.getError());
            }
        } finally {
            esignLock.releaseLock(lock);
        }

        long millisecond = Long.parseLong(accessToken.getExpiresIn());
        esignCache.put(EsignCache.TOKEN_KEY, accessToken, millisecond);

        return accessToken.getToken();
    }

    /**
     * 检查有效时间
     * @param accessToken 认证token
     * @return true有效, false无效
     */
    private boolean checkExpireTime(AccessToken accessToken) {
        if (Objects.isNull(accessToken)) {
            return false;
        }

        // 剩余有效时间要大于5分钟
        return Long.parseLong(accessToken.getExpiresIn()) - System.currentTimeMillis() > 1000 * 60 * 5;
    }

}